package jvm规范;

jvm结构 
本规范描述的是一种抽象化的虚拟机的行为，而不是任何一种（译者注：包括 Oracle 公司自己的 HotSpot 和 JRockit 虚拟机）被广泛使用的虚拟机实现。

记住：JVM规范是一种高度抽象行为的描述，而不是具体虚拟机的实现。

所有在虚拟机规范之中没有明确描述的实现细节，都不应成为虚拟机设计者发挥创造性的牵绊，设计者可以完全自主决定所有规范中不曾描述的虚拟机内部细节，例如：运行时数据区的内存如何布局、选用哪种垃圾收集的算法、是否要对虚拟机字节码指令进行一些内部优化操作（如使用即时编译器把字节码编译为机器码）。

简单地说，就是《JVM规范》中提到的内容，你一定要遵守。但没有提到的内容，你就自由发挥。所以我们要知道，我们经常听到的老年代、年轻代、永久代，其实只是HotSpot虚拟机的实现而已。因为《JVM规范》中并没有规定这些东西。

编译后被 Java 虚拟机所执行的代码使用了一种平台中立（不依赖于特定硬件及操作系统的）的二进制格式来表示，并且经常（但并非绝对）以文件的形式存储，因此这种格式被称为 Class文件格式。

实际上字节码文件，即Class文件格式使用十六进制编码，使用二进制格式存储。

Class 文件格式中精确地定义了类与接口的表示形式， 包括在平台相关的目标文件格式中一些细节上的惯例①，例如字节序（Byte Ordering）等。

这里又一个计算机的基础概念：字节序。有时间可以百度一下搞清楚这个概念。当然了，这个并不会影响对于JVM的理解。可以参考这篇文章：https://www.cnblogs.com/broglie/p/5645200.html

JVM的数据类型
与 Java 程序语言中的数据类型相似，Java 虚拟机可以操作的数据类型可分为两类：原始类型（Primitive Types，也经常翻译为原生类型或者基本类型）和引用类型（Reference Types）。与之对应，也存在有原始值（Primitive Values）和引用值（Reference Values）两种类型的数值。

JVM中也有两种类型：原始类型、引用类型。

Java 虚拟机希望尽可能多的类型检查能在程序运行之前完成，换句话说，编译器应当在编译
期间尽最大努力完成可能的类型检查，使得虚拟机在运行期间无需进行这些操作。

从这句话我们知道：在编译期间是会及逆行类型检查的，通过在编译进行类型检查，减少JVM运行时的工作量，提高效率。

Java 虚拟机所支持的原始数据类型包括了数值类型（Numeric Types）、 布尔类型（Boolean Type § 2.3.4） 和 returnAddress 类型（§ 2.3.3） 三类。其中数值类型又分为整型类型（Integral Types， § 2.3.1）和浮点类型（Floating-Point Types， § 2.3.2）两种。

和Java语言不同，JVM的原始类型包括：数值类型、布尔类型、returnAddress类型。

数值类型。又分为整数类型和浮点类型。整数类型包括：byet、short、int、long、char。浮点类型包括：float类型、double类型。
布尔类型。包括：boolean类型，取值为true和false，默认为false。
returnAddress类型。表示一条字节码指令的操作码。
可以看到在所有的虚拟机支持的原始类型之中，只有 returnAddress 类型是不能直接 Java 语言的数据类型对应起来的。

整数类型以及整型值
Java 虚拟机中的整型类型的取值范围如下：

对于 byte 类型，取值范围是从-128 至 127（-27至 27-1），包括-128 和 127。
对于 short 类型，取值范围是从−32768 至 32767（-215至 215-1），包括−32768 和 32767。
对于 int 类型，取值范围是从−2147483648 至 2147483647（-231至 231-1），包括−2147483648 和 2147483647。
对于 long 类型，取值范围是从−9223372036854775808 至 9223372036854775807（-263至 263-1），包括−9223372036854775808 和 9223372036854775807。
对于 char 类型， 取值范围是从 0 至 65535，包括 0 和 65535。

浮点类型的取值及范围
浮点类型包含 float 类型和 double 类型两种，它们在概念上与《IEEE Standard for Binary Floating-Point Arithmetic》 ANSI/IEEE Std. 754-1985（IEEE, New York）标准中定义的 32 位单精度和 64 位双精度 IEEE 754 格式取值和操作都是一致的。

浮点类型包括了 float 类型和 double 类型，它们是在 IEEE 754 标准中定义的。

所有 Java 虚拟机的实现都必须支持两种标准的浮点数值集合：单精度浮点数集合和双精度浮点数集合。

所有虚拟机都必须支持单精度浮点数集合和双精度浮点数集合。

关于浮点数的内容，其实看起来会非常晕。所以我们暂时可以大致看一下就可以，毕竟关于浮点数如何定义又是另一个标准了。

returnAddress 类型
returnAddress 类型会被 Java 虚拟机的 jsr、ret 和 jsr_w 指令所使用。jsr、ret、jsr_w 这几条指令以前主要被使用来实现 finally 语句块，后来改为冗余 finally 块代码的方式来实
现，甚至到了 JDK7 时，虚拟机已不允许 Class 文件内出现这几条指令。那相应地， returnAddress 类型就处于名存实亡的状态。

简单地说，returnAddress类型现在已经算是被抛弃了，所以我们只需要大致了解一下有这个东西就好了。

boolean类型
虽然 Java 虚拟机定义了 boolean 这种数据类型，但是只对它提供了非常有限的支持。在Java 虚拟机中没有任何供 boolean 值专用的字节码指令，在 Java 语言之中涉及到 boolean类型值的运算，在编译之后都使用 Java 虚拟机中的 int 数据类型来代替。

可以说，我们学会了int类型，就学会了boolean类型。因为boolean类型就是简化版的int类型。boolean的虚拟机指令集都是使用int类型的指令集。

引用类型与值
Java 虚拟机中有三种引用类型：类类型（Class Types）、数组类型（Array Types）和接口类型（Interface Types）。这些引用类型的值分别由类实例、数组实例和实现了某个接口的类实例或数组实例动态创建。

不仅原始类型不同，JVM的引用类型与原始类型也是不同的。JVM的引用类型有三种：

类类型。对应的值是类实例。
数组类型。对应的值时数组实例。
接口类型。对应的值时实现了某个接口的类实例或数组实例。
嗯，美滋滋啊。又学到新东西嘞。

数组类型还包含一个单一维度（即长度不由其类型决定）的组件类型（Component Type），一个数组的组件类型也可以是数组。但从任意一个数组开始，如果发现其组件类型也是数组类型的话，继续重复取这个数组的组件类型，这样操作不断执行，最终一定可以遇到组件类型不是数组的情况，这时就把这种类型成为数组类型的元素类型（Element Type）。数组的元素类型必须是原始类型、类类型或者接口类型之中的一种。

这段话很绕，其中有几个关键词：数组类型、组件类型（Component Type）。其实这段话意思应该是说数组可以有多维的意思，而数组里的类型又可以是其他各种类型。大致意思应该是如此，但我也不是100%确定。这里MARK一下。TODO

在引用类型的值中还有一个特殊的值： null，当一个引用不指向任何对象的时候，它的值就用 null 来表示。Java 虚拟机规范并没有规定 null 在虚拟机实现中应当怎样编码表示。

关于null的定义，了解一下。

运行时数据区
终于到运行时数据区了，其实这块就是我们经常说的JVM内存模型这些东西。但实际上JVM规范中并没有这个术语。JVM规范只定义了「运行时数据区」这个术语，指的就是JVM运行时其内存的数据区是怎么样的，应该包含哪些东西。具体怎么实现，你们各个虚拟机自己打算去。

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区，其中有一些会随着虚拟机启动而创建，随着虚拟机退出而销毁。另外一些则是与线程一一对应的，这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

为什么我们经常用JVM内存模型来概括运行时数据区，就是因为运行时数据区说的概念太过于分散，没有联系，所以才会有JVM内存模型这个词，让我们把这些东西联系起来，方便记忆。

从上面这段话，我们可以进行一些概括。首先第一句说到：

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区，其中有一些会随着虚拟机启动而创建，随着虚拟机退出而销毁。

意思是说有些东西会随着虚拟机启动而一直存在，而随着虚拟机退出而销毁。而另外一句：

另外一些则是与线程一一对应的，这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

上面这句意思是说，又有些数据是随着线程变化的。

其实总结一下就是：JVM运行时数据区有些数据是一直存在的，被所有线程共享的。而有些线程则是线程私有的，随着线程开始而创建，结束而销毁。所以，我们可以将运行时数据区的东西简单分为两类：一类是公有的，一类是私有的。

通过这样一归类，你对于运行时数据区的概念是不是清晰了许多呢！

可能你还没感觉到，那是因为到这里你还不知道运行时数据区到底有多少个东西。在这里我先列一下。运行时数据区包括下面几个部分：

PC寄存器
Java虚拟机栈
Java堆
方法区
运行时常量池
本地方法栈
好了。记住这 6 个东西还真是困难。那我们用我们上面说的，加个公有私有的分类试试看。

公有部分包括：Java堆、方法区、运行时常量池。
私有部分包括：Java虚拟机栈、本地方法栈、PC寄存器。


怎么样，这样一归类，我相信更容易记住了。而且也更符合我们对JVM的理解。JVM的运行时数据区有哪些东西？

首先，有公有和私有两个部分，公有包括……私有包括……。

大脑天然喜欢结构化的数据，这种方法才是理解运行时数据区的正确姿势。

说得有点多了，我们接下来继续读JVM规范。

PC寄存器
Java 虚拟机可以支持多条线程同时执行（可参考《Java 语言规范》第 17 章），每一条 Java虚拟机线程都有自己的 PC（Program Counter）寄存器。在任意时刻，一条 Java 虚拟机线程只会执行一个方法的代码，这个正在被线程执行的方法称为该线程的当前方法。

从这段描述验证上面我的理解是对的。PC寄存器就是线程私有的，每个线程都有一个PC寄存器。而PC寄存器是用来存储当前线程所执行方法的地址。

Java虚拟机栈
每一条 Java 虚拟机线程都有自己私有的 Java 虚拟机栈（Java Virtual Machine Stack）①，这个栈与线程同时创建，用于存储栈帧（Frames, § 2.6）。

从这段描述我们同样可以看到，Java虚拟机栈同样也是线程私有的。Java虚拟机栈的作用就是用来存储「栈帧」。栈帧这个概念也非常重要，其存储了调用方法时，方法的局部变量等信息。后续会深入学习「栈帧」这个概念，这里不做深入介绍。

如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时， Java 虚拟机将会抛出一个 StackOverflowError 异常。

传说中的StackOverflowError就是因为这块区域出现了问题。

如果 Java 虚拟机栈可以动态扩展，并且扩展的动作已经尝试过，但是目前无法申请到足够的内存去完成扩展，或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈，那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

这里很有意思。意思是说OutOfMemoryError异常，有可能是因为Stack不够导致的。

我相信许多对于JVM理解不够深刻的朋友看到这里会很疑惑，因为他们对于JVM内存模型的理解就是：Java堆、Java栈。但实际上并不是这样的。

你应该这样理解：首先，你得知道「运行时数据区」，就是JVM运行时的这块内存。之后，你要知道这块区域分为两个部分：公有和私有。而Java虚拟机栈是分配在堆上的（注意，并不是Java堆），但分配出来的这块内存是线程私有的。

在创建线程是申请创建Java虚拟机栈，可能有两种情况。第一种，堆上的内存不够了，那么无法创建Java虚拟机栈。这就是上面说到的这种情况，会发生OutOfMemoryError异常。另一种情况，就是内存申请成功了，但是在线程创建后，调用方法的层次太深了，之前申请的这部分内存不够用了。这时候会发生StackOverflowError。

说到这儿，你会发现在《JVM规范》第28页末尾写了这么一段话：

译者注：请读者注意避免混淆 Stack、 Heap 和 Java （VM） Stack、 Java Heap 的概念， Java 虚拟机的实现本身是由其他语言编写的应用程序，在 Java 语言程序的角度上看分配在 Java Stack 中的数据，而在实现虚拟机的程序角度上看则可以是分配在 Heap 之中。

这段话真是不太好理解，但实际上没那么难。首先，我们的JVM是使用其他语言写的，那么这个要运行的时候是不是要像系统申请内存，那么申请的这一大块内存就是堆内存。

在这么大一块内存里，我们又分一部分作为Java堆内存、Java虚拟机栈的内存。也即是说，JVM中的堆内存是系统的堆内存中的堆内存。站在实现JVM的程序来说，是分配在堆上的。而站在Java程序的角度上看，是分配在Java堆上的。也就是说可以想象这样一个图表：

系统内存有堆内存、栈内存之分。
JVM启动，向系统申请一块内存，系统会分配一块堆内存给它。当其他应用程序，例如微信启动，系统也会分配一块堆内存给它。
JVM获得了内存，于是加载类、运行程序。JVM会在获得的内存分出一部分内存作为Java堆，用于分配对象。
当创建线程，那么JVM会在系统给它的、还未分配的内存划出一部分创建Java虚拟机栈，从而线程创建成功。
所以如果增加Java堆的大小，那么可以分配给Java虚拟机栈的内存就变少，那么可以创建的线程就减少。那么就更容易导致StackOverflowError。

Java堆
在 Java 虚拟机中，堆（Heap）是可供各条线程共享的运行时内存区域，也是供所有类实例和数组对象分配内存的区域。

Java堆是所有线程共享的，存放类实例和数组对象。

方法区
在 Java 虚拟机中，方法区（Method Area） 是可供各条线程共享的运行时内存区域。方法区与传统语言中的编译代码储存区（Storage Area Of Compiled Code）或者操作系统进程的正文段（Text Segment）的作用非常类似，它存储了每一个类的结构信息，例如运行时常量池（Runtime Constant Pool）、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法（§ 2.9）。

可以看到方法区也是共享的，而且存放的是类的信息。在HotSpot虚拟机中，JDK1.7版本称其为永久代（Permanent Generation），而在JDK1.8则称之为元空间（Metaspace）。

运行时常量池
每一个运行时常量池都分配在 Java 虚拟机的方法区之中（§ 2.5.4），在类和接口被加载到虚拟机后，对应的运行时常量池就被创建出来。

运行时常量池分配在JVM的方法区之中。

本地方法栈
Java 虚拟机实现可能会使用到传统的栈（通常称之为“C Stacks”）来支持 native 方法（指使用 Java 以外的其他语言编写的方法）的执行，这个栈就是本地方法栈（Native Method Stack）。当 Java 虚拟机使用其他语言（例如 C 语言）来实现指令集解释器时，也会使用到本地方法栈。

本地方法指的是使用Java以外的其他语言编写的代码，因为有些时候Java无法直接操作一些底层资源，只能通过C或汇编操作。因此需要通过本地方法来实现。

而本地方法栈就是设计用来调用这些非Java语言方法的，其作用与Java虚拟机栈类似。会存放对应的局部变量信息、返回结果等。

本地方法栈同样会发生StackOverFlowError和OutOfMemoryError异常。

栈帧
栈帧（Frame）是用来存储数据和部分过程结果的数据结构，同时也被用来处理动态链接（Dynamic Linking）、方法返回值和异常分派（Dispatch Exception）。

这里重点记住：存储数据和部分结果，存储的部分数据就包括了局部变量。

栈帧随着方法调用而创建，随着方法结束而销毁——无论方法是正常完成还是异常完成（抛出了在方法内未被捕获的异常）都算作方法结束。

栈帧随着方法调用而创建，是线程私有的。

栈帧的存储空间分配在 Java 虚拟机栈（§ 2.5.5）之中，每一个栈帧都有自己的局部变量表（Local Variables， § 2.6.1）、操作数栈（Operand Stack， § 2.6.2）和指向当前方法所属的类的运行时常量池（§ 2.5.5）的引用。

栈帧分配在Java虚拟机栈，而Java虚拟机栈是私有的，所以栈帧肯定也是私有的。可以这么说，栈帧就是Java虚拟机栈里的一个个元素，每次调用一个方法就push一个栈帧，调用完毕则poll一个栈帧。

这里还说到，每一个栈帧都有自己的：

局部变量表
操作数栈
指向当前方法所属类的运行时常量池引用
在一条线程之中，只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧（Current Frame），这个栈帧对应的方法就被称为是当前方法（Current Method），定义这个方法的类就称作当前类（Current Class）。

了解下「当前栈帧」、「当前方法」、「当前类」的概念。

请读者特别注意，栈帧是线程本地私有的数据，不可能在一个栈帧之中引用另外一条线程的栈帧。

这里原文明确指出：栈帧是线程本地私有的数据！

局部变量表
栈帧中局部变量表的长度由编译期决定，并且存储于类和接口的二进制表示之中，既通过方法的Code 属性（§ 4.7.3）保存及提供给栈帧使用。

局部变量表由编译期决定，并存在方法的Code属性。

局部变量使用索引来进行定位访问，第一个局部变量的索引值为零，局部变量的索引值是从零至小于局部变量表最大容量的所有整数。

局部变量表的索引从零开始。

特别地，当一个实例方法被调用的时候，第 0 个局部变量一定是用来存储被调用的实例方法所在的对象的引用（即 Java 语言中的“this”关键字）。

局部变量第一个局部变量，一定是当前对象的引用，即this关键字。

操作数栈
每一个栈帧（§ 2.6）内部都包含一个称为操作数栈（Operand Stack）的后进先出（Last-In-First-Out， LIFO）栈。

这里要注意一下，Java虚拟机栈包含了栈帧，栈帧包含了操作数栈，就像下面这样的关系：

Java虚拟机栈 -> 栈帧 -> 操作数栈
它们都是栈的数据结构，操作数栈也是如此。

只不过Java虚拟机栈是存储一个个线程的信息，栈帧存储的事一个个方法的信息，操作数栈是存储方法调用中一个个操作数的信息。

每一个操作数栈的成员（Entry） 可以保存一个 Java 虚拟机中定义的任意数据类型的值，包括 long 和 double 类型。

操作数栈的成员可以是任意数据类型。

这章节接下来的关于浮点数、浮点算法部分，太过于复杂，而且实用性太差。这里不深入了解，直接简单了解就可以了。

在 Java 虚拟机层面上， Java 语言中的构造函数在《Java 语言规范 （第三版）》（下文简称JLS3， § 8.8）是以一个名为的特殊实例初始化方法的形式出现的， 这个方法名称是由编译器命名的，因为它并非一个合法的 Java 方法名字，不可能通过程序编码的方式实现。

<init>这个特殊的初始化方法，是在虚拟机层面存在的，而不是程序编码层面的。可以通过虚拟机的invokespecial指令调用。

一个类或者接口最多可以包含不超过一个类或接口的初始化方法，类或者接口就是通过这个方法完成初始化的（§ 5.5）。这个方法是一个不包含参数的静态方法，名为①。这个名字也是由编译器命名的，因为它并非一个合法的 Java 方法名字，不可能通过程序编码的方式实现。

与init类似clinit也是一个虚拟机层面的方法，不是程序编码层面的。但类或接口的clinit方法只能由虚拟机自身隐式调用，没有任何虚拟机字节码指令可以调用这个方法。

字节码指令
Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作（Opcode）以及跟随其后的零至多个代表此操作所需参数的操作数（Operands）所构成。

字节码指令组成为：操作码+操作数。其中操作码为一个字节长度，操作数情况由操作码决定。

大部分的指令都没有支持整数类型 byte、 char 和 short，甚至
没有任何指令支持 boolean 类型。编译器会在编译期或运行期会将 byte 和 short 类型的数据带符号扩展（Sign-Extend）为相应的 int 类型数据，将 boolean 和 char 类型数据零位扩展（Zero-Extend）为相应的 int 类型数据。与之类似的，在处理 boolean、 byte、 short 和char 类型的数组时，也会转换为使用对应的 int 类型的字节码指令来处理。

在虚拟机中，byte/short/boolean/char 都是用 int 类型来存储的。规范的下一句也直接指明了。

因此，大多数对于boolean、 byte、 short 和 char 类型数据的操作，实际上都是使用相应的对 int 类型作为运算类型（Computational Type）。

虚拟机指令集大致可以分为下面几类：

加载和存储指令
运算指令
类型转换指令
对象创建于操作
操作数栈管理指令
控制转移指令
方法调用和返回指令
抛出异常
同步
因为虚拟机指令集很枯燥，就像linux命令一样，所以这里不深入讲。有需要的时候再一个个查就可以了。